package org.haptimap.hcimodules.pocketmenu;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Vibrator;
import android.speech.tts.TextToSpeech;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView;

/**
 * The main class of the PocketMenu. To use it in your application, add
 * 
 * <code>
 * <org.haptimap.hcimodules.pocketmenu.PocketMenu
 * 		android:id="@+id/pocketmenu" 
 * 		android:layout_width="fill_parent"
 * 		android:layout_height="fill_parent"	/>
 * </code>
 * 
 * to your layout.xml. In the code obtain a reference via findViewById and then
 * add buttons via #addButton(..).
 * 
 * <code>
 * this.pocketMenu = (PocketMenu) findViewById(R.id.pocketmenu);
 * this.pocketMenu.addListener(this);
 * 
 * this.button = pocketMenu.addButton("Button", R.drawable.button);
 * </code>
 * 
 * 
 * @author <a href="mailto:"martin.pielot@offis.de">Martin Pielot</a>
 * @version Nov 06, 2010
 */
public class PocketMenu extends View implements OnTouchListener {

	// ========================================================================
	// Constant Fields
	// ========================================================================

	private static final String TAG = "PocketMenu";

	private static final double DEFAULT_BUTTON_SIZE_PERCENTAGE = 0.20;

	/**
	 * Number of pixels a touch can move but still is considered as
	 * "staying in the same place". Used to discover long-click events.
	 */
	private static final int NOT_MOVING_MARGIN = 5;

	private static final long SELECT_BUTTON_MS = 50;
	private static final long PRESS_BUTTON_MS = 100;
	private static final long RELEASE_SLIDER_MS = 30;
	private static final long SLIDER_CATEGORY_MS = 30;

	private static final int BUTTON_LONG_CLICK_COUNT = 50;
	private static final int SLIDER_LONG_CLICK_COUNT = 25;

	// ========================================================================
	// Fields
	// ========================================================================

	// Menu visualization
	private List<PocketMenuItem> items = new LinkedList<PocketMenuItem>();
	private double buttonSizePercentage = DEFAULT_BUTTON_SIZE_PERCENTAGE;
	private int buttonSize;

	// External configuration
	private boolean alwaysVisible = true;
	private boolean consumeAllEvents = true;
	private boolean rightHanded = true;
	private boolean speakItemOnSelection = true;
	private boolean speakItemOnLongClick = true;
	private boolean stopSpeechOnTouchUp = true;

	// Touch event handling
	private boolean menuVisible;
	private Point lastTouchPoint = new Point();
	private PocketMenuItem lastTouchedItem;
	private PocketMenuItem selectedItem;
	private PocketMenuSlider selectedSlider;
	private int longClickCounter = 0;

	// UI
	private Vibrator vib;
	private TextToSpeech tts;

	// Listener
	private List<PocketMenuListener> listeners;

	// Tutorial
	private Paint tutorialPaint;
	private TextView textView;
	private boolean tutorialVisible;

	// ========================================================================
	// Constructor
	// ========================================================================

	public PocketMenu(Context context) {
		super(context);
		Log.i(TAG, "PocketMenu(Context)");
		init(context);
	}

	public PocketMenu(Context context, AttributeSet attrs) {
		super(context, attrs);
		Log.i(TAG, "PocketMenu(Context, AttributeSet)");
		init(context);
	}

	public PocketMenu(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs);
		Log.i(TAG, "PocketMenu(Context, AttributeSet, int)");
		init(context);
	}

	public void init(Context context) {
		try {
			this.listeners = new ArrayList<PocketMenuListener>();
			this.setWillNotDraw(false);
			this.setButtonSizePercentage(DEFAULT_BUTTON_SIZE_PERCENTAGE);
			this.menuVisible = alwaysVisible;

			this.vib = (Vibrator) context
					.getSystemService(Context.VIBRATOR_SERVICE);

			this.setOnTouchListener(this);

			// Tutorial Paint for the Text
			this.tutorialPaint = new Paint();
			this.tutorialPaint.setColor(Color.parseColor("#222222"));
			this.tutorialPaint.setTextSize(16);

		} catch (Exception e) {
			Log.w(TAG, e + " in init()", e);
		}
	}

	// ========================================================================
	// Public Methods
	// ========================================================================

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.haptimap.offis.pocketmenu.Evaluationable#setButtonSizePercentage(
	 * double)
	 */
	public void setButtonSizePercentage(double buttonSizePercentage) {
		this.buttonSizePercentage = ensureWithin(buttonSizePercentage, 0, 1);
		this.buttonSize = (int) (getWidth() * buttonSizePercentage);
	}

	public PocketMenuButton addButton(String name, int drawableId) {
		PocketMenuButton button = new PocketMenuButton(this.getContext(), name,
				drawableId);
		this.items.add(button);
		return button;
	}

	public PocketMenuSlider addSlider(String name, int drawableId,
			boolean immedateCallback) {
		PocketMenuSlider slider = new PocketMenuSlider(this.getContext(), name,
				drawableId, immedateCallback);
		this.items.add(slider);
		return slider;
	}

	public void setTextToSpeech(TextToSpeech tts) {
		this.tts = tts;
	}

	public void addListener(PocketMenuListener listener) {
		this.listeners.add(listener);
	}

	public void removeListener(PocketMenuListener listener) {
		this.listeners.remove(listener);
	}

	public void setSpeakItemsOnSelection(boolean speakItems) {
		this.speakItemOnSelection = speakItems;
	}

	public boolean isSpeakItemsOnSelection() {
		return speakItemOnSelection;
	}

	public void setSpeakItemOnLongClick(boolean speakItems) {
		this.speakItemOnLongClick = speakItems;
	}

	public boolean isSpeakItemOnLongClick() {
		return speakItemOnLongClick;
	}

	public void setAlwaysVisible(boolean alwaysVisible) {
		this.alwaysVisible = alwaysVisible;
		this.menuVisible = alwaysVisible;
		this.invalidate();
	}

	public boolean isAlwayVisible() {
		return alwaysVisible;
	}

	public boolean isConsumeAllEvents() {
		return consumeAllEvents;
	}

	public void setConsumeAllEvents(boolean consumeAllEvents) {
		this.consumeAllEvents = consumeAllEvents;
	}

	public void setStopSpeechOnTouchUp(boolean stopSpeechOnTouchUp) {
		this.stopSpeechOnTouchUp = stopSpeechOnTouchUp;
	}

	public boolean isStopSpeechOnTouchUp() {
		return stopSpeechOnTouchUp;
	}

	public void setRightHanded(boolean rightHanded) {
		this.rightHanded = rightHanded;
		if (!rightHanded)
			Log.w(TAG, "left hand version does not yet work!");
	}

	public void setTutorialVisible(boolean tutorialVisible) {
		this.tutorialVisible = tutorialVisible;
		if (textView != null) {
			int visibility = tutorialVisible ? View.VISIBLE : View.INVISIBLE;
			this.textView.setVisibility(visibility);
			this.invalidate();
		}
	}

	public void setTutorialText(String tutorialText) {
		if (textView != null) {
			this.textView.setText(tutorialText);
			this.textView.invalidate();
		}
		this.speak(tutorialText);
		this.invalidate();
	}

	public void setTutorialTextView(TextView textView) {
		if (textView != null) {
			this.textView = textView;
			this.textView.setText("Text for the PocketMenu tutorial");
			this.setTutorialVisible(this.tutorialVisible);
		} else {
			Log.w(TAG, "Tutorial TextView is NULL");
		}
	}

	// ------------------------------------------------------------------------
	// Listener notification
	// ------------------------------------------------------------------------

	private void notifyOnButtonPressed(PocketMenuButton button) {
		Log.i(TAG, button.getName() + " button 'pressed'");
		for (PocketMenuListener listener : listeners) {
			listener.onInteractedWithItem(button);
		}

	}

	private void notifyOnSliderChanging(PocketMenuSlider slider, double value) {

		boolean categoryChanged = false;

		for (PocketMenuListener listener : listeners) {
			categoryChanged = categoryChanged
					|| listener.onSliderChanging(slider, value);
		}

		if (categoryChanged) {
			this.vib.vibrate(SLIDER_CATEGORY_MS);
		}
	}

	private void notifyOnSliderChanged(PocketMenuSlider slider, double value) {
		for (PocketMenuListener listener : listeners) {
			listener.onSliderChanged(slider, value);
		}
	}

	// ------------------------------------------------------------------------
	// Implementation of 'View'
	// ------------------------------------------------------------------------

	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {

		if (changed) {

			Log.i(TAG, "onLayout( " + changed + ", " + left + ", " + top + ", "
					+ right + ", " + bottom + ")");

			// Update the button size
			this.buttonSize = (int) (this.getWidth() * buttonSizePercentage);
			final int height = bottom - top;
			double maxButtonSize = height / this.items.size();
			if (buttonSize > maxButtonSize) {
				double newPerc = maxButtonSize / getWidth();
				this.setButtonSizePercentage(newPerc);
			}

			layoutItems();
		}
	}

	private void layoutItems() {
		final int bottom = this.getHeight();
		final int left = rightHanded ? getLeft() : getRight() - buttonSize;

		for (int i = 0; i < items.size(); i++) {
			PocketMenuItem item = items.get(i);

			Rect bounds = new Rect(left,
					(bottom - buttonSize * i - buttonSize), left + buttonSize,
					bottom - buttonSize * i);

			item.setBounds(bounds);
		}
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		// super.onDraw(canvas);

		if (menuVisible) {
			for (PocketMenuItem item : items) {
				Drawable icon = item.getIcon();
				icon.draw(canvas);
			}
		}
	}

	// ------------------------------------------------------------------------
	// Implementation of 'View.OnTouchListener'
	// ------------------------------------------------------------------------

	public boolean onTouch(View v, MotionEvent event) {

		int action = event.getAction();
		int x = (int) event.getX();
		int y = (int) event.getY();

		int travelledDistance = (int) travelledDistance(x, y);
		boolean longClick = waitForLongClick(travelledDistance);

		// find out which item is being touched (NULL when none)
		final PocketMenuItem touchedItem = getTouchedItem(x, y);
		// Log.v(TAG, action + "\t" + x + "/" + y + "\t" + touchedItem);

		// if an item is being touched AND no slider is selected
		if (touchedItem != null) {

			/*
			 * First we have to test if there is not selected item OR the item
			 * has just been touched OR the touched item is another one than the
			 * selected item ..
			 */
			if (selectedItem == null || lastTouchedItem == null
					|| selectedItem != touchedItem) {

				/*
				 * Now we have to test if the menu is visible or the "home" item
				 * has been touched
				 */
				if (menuVisible || touchedItem == items.get(0)) {

					/*
					 * Check whether user is interacting with a slider to avoid
					 * re-selecting the slider.
					 */
					if (selectedSlider == null) {
						// make the menu visible
						this.menuVisible = true;
						this.invalidate();

						// select the current item!
						this.onItemSelected(touchedItem);
					}
				}
			}
		}

		// if an item is selected lets check whether the user interacts with it
		if (selectedItem != null && selectedSlider == null) {

			if (longClick && speakItemOnLongClick) {
				speakDetailedDescription(selectedItem);
			}

			// Determine x distance from item's center
			Point p = selectedItem.getCenter();
			int xDistance = Math.abs(p.x - x);

			// if we have moved further than the item size
			if (xDistance > buttonSize) {

				// interact with item
				this.onInteractWithItem(selectedItem);
			}
		}

		if (selectedSlider != null) {

			if (longClick && speakItemOnLongClick) {
				speakDetailedDescription(selectedSlider);
			}

			double value = getSliderValue(x);
			selectedSlider.setValue(value);
			if (travelledDistance > 0) {
				this.vib.vibrate(travelledDistance / 2 + 1);
				notifyOnSliderChanging(selectedSlider,
						selectedSlider.getValue());
			}
		}

		// Remember which item was touch in the last call of onTouch(..) and
		// where the touch occurred
		this.lastTouchedItem = touchedItem;
		this.lastTouchPoint.x = x;
		this.lastTouchPoint.y = y;

		// if the user lifts the finger we reset all temporary items
		if (action == MotionEvent.ACTION_UP) {

			if (selectedSlider != null) {
				this.vib.vibrate(RELEASE_SLIDER_MS);
				notifyOnSliderChanged(selectedSlider, selectedSlider.getValue());
			}

			// reset temporary items
			this.lastTouchedItem = null;
			this.selectedItem = null;
			this.selectedSlider = null;

			// if menu is not always visible make it invisible
			this.menuVisible = alwaysVisible;
			this.invalidate();

			// interrupt speach
			if (stopSpeechOnTouchUp) {
				this.tts.stop();
			}
		}

		// Consume touch events when the menu is visible
		return consumeAllEvents || menuVisible;
	}

	// ------------------------------------------------------------------------
	// Touch event handling
	// ------------------------------------------------------------------------

	private void onItemSelected(PocketMenuItem item) {
		this.selectedItem = item;
		Log.i(TAG, "selected item " + item);
		this.vib.vibrate(SELECT_BUTTON_MS);
		if (speakItemOnSelection) {
			this.speak(item.getName());
		}

		for (PocketMenuListener listener : listeners) {
			listener.onItemSelected(item);
		}
	}

	private void onInteractWithItem(PocketMenuItem item) {

		this.vib.vibrate(PRESS_BUTTON_MS);
		Log.i(TAG, "interacting with " + item);

		if (item instanceof PocketMenuButton) {
			this.selectedItem = null;
			this.notifyOnButtonPressed((PocketMenuButton) item);

		} else if (item instanceof PocketMenuSlider) {
			this.selectedSlider = (PocketMenuSlider) item;
		}
	}

	private double getSliderValue(int x) {
		int border = rightHanded ? this.getLeft() : this.getRight();
		double sliderPosition = Math.abs(border - x);
		double value = sliderPosition / this.getWidth();
		return ensureWithin(value, 0, 1);
	}

	private boolean waitForLongClick(int travelledDistance) {

		// Long click may only occur when item is selected
		if (selectedItem == null)
			return false;

		// Increase counter if user has not moved. Reset counter otherwise.
		if (travelledDistance <= NOT_MOVING_MARGIN) {
			longClickCounter++;
		} else {
			longClickCounter = 0;
		}

		// See if long-click has occurred
		if (selectedItem instanceof PocketMenuButton) {
			return longClickCounter == BUTTON_LONG_CLICK_COUNT;
		} else if (selectedItem instanceof PocketMenuSlider) {
			return longClickCounter == SLIDER_LONG_CLICK_COUNT;
		}

		return false;
	}

	private double travelledDistance(int x, int y) {
		int xv = lastTouchPoint.x - x;
		int yv = lastTouchPoint.y - y;
		return Math.sqrt(xv * xv + yv * yv);
	}

	private PocketMenuItem getTouchedItem(int x, int y) {
		for (PocketMenuItem item : items) {
			if (item.contains(x, y)) {
				return item;
			}
		}
		return null;
	}

	// ------------------------------------------------------------------------
	// Helpers
	// ------------------------------------------------------------------------

	private void speakDetailedDescription(PocketMenuItem item) {

		String s = item.getDetailedDescription();
		if (listeners.size() > 0 && item instanceof PocketMenuSlider) {
			PocketMenuListener listener = listeners.get(0);
			PocketMenuSlider slider = (PocketMenuSlider) item;
			s = listener.getSliderStateDescription(slider, slider.getValue());
		}

		speak(s);
	}

	private void speak(final String s) {
		// Log.i(TAG, "speaking '" + s + "'");
		if (tts != null) {
			tts.speak(s, TextToSpeech.QUEUE_FLUSH, null);
		}
	}

	static double ensureWithin(double value, double min, double max) {
		if (value > max)
			return max;

		if (value < min)
			return min;

		return value;
	}

	static String toString(double percent) {
		int i = (int) (percent * 100);
		return i + "%";
	}

}
